
package com.x52im.rbchat.logic.chat_root;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.PowerManager;
import android.text.Editable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ImageSpan;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MotionEvent;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Toast;

import com.bumptech.glide.Glide;
import com.eva.android.widget.DataLoadableActivity;
import com.eva.android.ToolKits;
import com.eva.android.widget.WidgetUtils;
import com.eva.android.widget.alert.AlertDialog;
import com.eva.epc.common.util.CommonUtils;
import com.eva.framework.dto.DataFromServer;
import com.noober.floatmenu.FloatMenu;
import com.noober.floatmenu.MenuItem;
import com.x52im.rbchat.Const;
import com.x52im.rbchat.MyApplication;
import com.x52im.rbchat.R;
import com.x52im.rbchat.cache.ImageCacheLoader;
import com.x52im.rainbowchat.http.logic.dto.GroupEntity;
import com.x52im.rainbowchat.http.logic.dto.RosterElementEntity;
import com.x52im.rainbowchat.im.dto.ChatType;
import com.x52im.rainbowchat.im.dto.MsgType;
import com.x52im.rainbowchat.im.dto.RevokedMeta;
import com.x52im.rbchat.logic.alarm.AlarmType;
import com.x52im.rbchat.cache.AlarmsProvider;
import com.x52im.rbchat.cache.MessagesProvider;
import com.x52im.rbchat.logic.chat_friend.utils.MessageForwardHelper;
import com.x52im.rbchat.logic.chat_friend.utils.MessageHelper;
import com.x52im.rbchat.cache.GroupsProvider;
import com.x52im.rbchat.logic.chat_group.utils.GMessageForwardHelper;
import com.x52im.rbchat.logic.chat_group.utils.GMessageHelper;
import com.x52im.rbchat.logic.chat_guest.utils.TMessageForwardHelper;
import com.x52im.rbchat.logic.chat_guest.utils.TMessageHelper;
import com.x52im.rbchat.logic.chat_root.face.EmojiUtil;
import com.x52im.rbchat.logic.chat_root.face.FaceBoardView;
import com.x52im.rbchat.logic.chat_root.impl.MessageRevokingDialogProgess;
import com.x52im.rbchat.logic.chat_root.impl.MessageRevokingManager;
import com.x52im.rbchat.logic.chat_root.impl.Quote4InputWrapper;
import com.x52im.rbchat.logic.chat_root.model.ContactMeta;
import com.x52im.rbchat.logic.chat_root.model.FaceEntity;
import com.x52im.rbchat.logic.chat_root.model.LocationMeta;
import com.x52im.rbchat.logic.chat_root.model.Message;
import com.x52im.rbchat.logic.chat_root.model.MessageExt;
import com.x52im.rbchat.logic.chat_root.sendshortvideo.SendShortVideoProcessor;
import com.x52im.rbchat.logic.chat_root.targetchoose.TargetChooseActivity;
import com.x52im.rbchat.logic.chat_root.targetchoose.TargetChooseResultConfirm;
import com.x52im.rbchat.logic.chat_root.targetchoose.TargetEntity;
import com.x52im.rbchat.logic.chat_root.targetchoose.TargetSource;
import com.x52im.rbchat.logic.chat_root.targetchoose.TargetSourceFilterFactory;
import com.x52im.rbchat.utils.ActivityRequestCode;
import com.x52im.rbchat.utils.BroadcastToolKits;
import com.x52im.rbchat.utils.IntentFactory;
import com.x52im.rbchat.utils.SoftKeyboardStateHelper;

import net.x52im.mobileimsdk.server.protocal.Protocal;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;

import androidx.core.app.ActivityCompat;

/**
 * 聊天界面的根类。
 *
 * @author JackJiang
 * @since 7.3
 */
public abstract class AbstractChattingActivity extends DataLoadableActivity {

    private final static String TAG = AbstractChattingActivity.class.getSimpleName();

    /** Intent数据KEY常量定义：给定指纹码的消息将高亮显示一次（当前用于搜索功能进入聊天界面时） */
    public final static String INTENT_KEY_HIGHLIGHT_ONECE_MSG$FINGERPRINT = "__highlight_msg_fp__";

    // 屏幕常亮管理
    private PowerManager powerManager = null;
    private PowerManager.WakeLock wakeLock = null;

    /** 常量：复制消息的长按菜单id */
    protected final int CONTEXT_MENU_ID_COPY = Menu.FIRST;
    /** 常量：撤回消息的长按菜单id */
    protected final int CONTEXT_MENU_ID_REVOKE = Menu.FIRST + 1;
    /** 常量：删除消息的长按菜单id */
    protected final int CONTEXT_MENU_ID_DELETE = Menu.FIRST + 2 ;
    /** 常量：转发消息的长按菜单id */
    protected final int CONTEXT_MENU_ID_FORWARD = Menu.FIRST + 3 ;
    /** 常量：引用消息的长按菜单id */
    protected final int CONTEXT_MENU_ID_QUOTE = Menu.FIRST + 4 ;
    /** 仿微信的长按弹出浮动菜单的显示参考基准位置 */
    protected Point floatMenuShowPoint = new Point();

    /** 聊天类型。 @see {@link ChatType} */
    protected int chatType = -1;
    /** 聊天id。一对一聊天时是存放的是对方的uid，群聊时是群id */
    protected String toId = null;
    /** 聊天名。一对一陌生人聊天时是存放的是对方的昵称，群聊时是群名称 */
    protected String toName = null;

    /** 消息"撤回"功能对应的进度提示框 */
    protected MessageRevokingDialogProgess messageRevokingDialogProgess = null;

    /** 该指纹码的消息将高亮显示一次（该指纹码当前通过Intent传入，当前主要用于搜索功能中进入聊天界面时） */
    protected String highlightOnceMsgFingerprint = null;

    /** 消息引用功能的输入框功能封装类 */
    protected Quote4InputWrapper quote4InputWrapper = null;

//    protected boolean softKeyboardOpened = false;

    //=============================================================== 广播通知相关 START
    /**
     * 广播监听者："位置"消息中的预览截图保存完成时的广播通知。
     *
     * （由于高德地图的位置预览截图功能是由高德地图库内部完成，且是完全异步——不受开发者的程序控制，
     *  那么存在一个问题：即当位置消息指令已经发出时(只需很短的时间)，而此截图很可能还并保存完成
     *  (尤其在一些低端机型上)，为了解决这个问题，就是在此图保存完成时发出一个广播，UI界面收到时
     *  强行刷新一次，就能弥补本地刚才未加载到此预览图的问题了）*/
    private BroadcastReceiver locationMsgPreviewImgUploadedBR = null;

    /**
     * 广播监听者：消息"撤回"功能中当收到撤回指令的应答时的广播通知.
     * <p>
     * 由于消息"撤回"功能需要明确得到撤回指令是否被送达（通过MobileIMSDK的QoS机制确保），以便UI进行
     * 即时更新（比如像微信那样撤回时有进度提示，撤回完成后及时取消进度提示），本广播就是为了在收到指令
     * 应答后通过广播通知Ui层刷新（广播这种机制可以让后台数据与前端UI进行优雅解偶）。*/
    protected BroadcastReceiver revokeCMDRecievedBR = null;
    //=============================================================== 广播通知相关 END

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 保持屏幕长亮，提升使用体验
        this.powerManager = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
        this.wakeLock = this.powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "Rainbowchat:My Lock");

        // 初始化引用功能的输入框功能封装类（本类需要在子类的initeViews()方法被调用才能实例化，所以放在onCreate中）
        this.quote4InputWrapper = new Quote4InputWrapper(this);

//        SoftKeyboardStateHelper mKeyboardHelper = new SoftKeyboardStateHelper(this.getWindow().getDecorView());
//        mKeyboardHelper.addSoftKeyboardStateListener(new SoftKeyboardStateHelper.SoftKeyboardStateListener() {
//            @Override
//            public void onSoftKeyboardOpened(int keyboardHeightInPx) {
//                softKeyboardOpened = true;
//            }
//
//            @Override
//            public void onSoftKeyboardClosed() {
//                softKeyboardOpened = false;
//            }
//        });
    }

    /**
     * 本activity被销毁时额外要做一些事.
     */
    @Override
    protected void onDestroy() {

        // 尝试强行重置消息高亮
        // FIXME: 20220919 优化了 WidgetUtils.highlightOnceMessageItem(..)中"message.setHighlightOnce(false);"
        //        的调用时机后（之前是动一结束才调用），貌似本方法及其调用就不需要了，日后评估若真不需要，则此代码可以去掉了！
        undoHighlightOnceMessage();

        // 取消注册广播监听者：此广播是"位置"消息中的预览截图保存完成时的广播通知
        BroadcastToolKits.locationMsgPreviewImgUploaded_UNREGISTER(this, locationMsgPreviewImgUploadedBR);
        // 取消注册广播监听者：此广播是消息"撤回"功能中当收到撤回指令的应答时的广播通知
        BroadcastToolKits.revokeCMDRecieved_UNREGISTER(this, revokeCMDRecievedBR);

        super.onDestroy();
    }

    /**
     * 根据Activity的生命周期：本Activity每次从后台回到前台时调用本方法。
     */
    protected void onResume() {
        super.onResume();
        // 开始屏幕长亮
        this.wakeLock.acquire();
    }

    /**
     * 根据Activity的生命周期：本Activity每次从前台退到后台时调用本方法。
     */
    protected void onPause() {
        super.onPause();
        // 取消屏幕长亮
        this.wakeLock.release();
    }

    /**
     * 从Intent中读取传递过来的数据。
     */
    @Override
    protected void initDataFromIntent() {
        // 尝试从Intent中读取搜索功能传进来的将要被高亮显示一次的消息指纹码（目前仅在从搜索功能界面中进入到聊天界面时才传入）
        highlightOnceMsgFingerprint = getIntent().getStringExtra(INTENT_KEY_HIGHLIGHT_ONECE_MSG$FINGERPRINT);
    }

    /**
     * 本方法由父类的onCreate()调用，子类可在此方法中实现自已的UI显示逻辑。
     */
    @Override
    protected void initViews(Bundle savedInstanceState) {
        // 进到本界面时不调用父类loadData方法（因为用类中用不到）
        this.setLoadDataOnCreate(false);

        //** 初始化广播事件监听器
        this.initBroadCastReciever();
    }

    /**
     * 初始化各广播监听器。
     */
    protected void initBroadCastReciever() {
        locationMsgPreviewImgUploadedBR = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String fname = intent.getStringExtra("fname");
                Log.i(TAG, "【聊天界面-收到位置消息地图预览图"+fname+"保存完成的通知！】");
                // 刷新界面，确保本地位置消息的预览图能及时显示出来
                notifyListDataSetChanged();
            }
        };
        // 注册广播监听者：用于接收"位置"消息中的预览截图保存完成时的广播通知
        BroadcastToolKits.locationMsgPreviewImgUploaded_REGISTER(this, locationMsgPreviewImgUploadedBR);

        revokeCMDRecievedBR = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String fpForRevokeCMD = intent.getStringExtra("fp_for_revoke_cmd");
                String fpForMessage = intent.getStringExtra("fp_for_message");
                Log.i(TAG, "【消息撤回】收到 fpForMessage="+fpForMessage+"、fpForRevokeCMD="+fpForRevokeCMD+" 已通过QoS确认送达的广播通知！");

                // 取消消息撤回进度提示框的显示
                hideMessageRevokingProgess(false, fpForMessage);

                // 刷新聊天列表UI显示
                notifyListDataSetChanged();
            }
        };
        // 注册广播监听者：用于接收消息"撤回"功能中当收到撤回指令的应答时的广播通知
        BroadcastToolKits.revokeCMDRecieved_REGISTER(this, revokeCMDRecievedBR);
    }

    /**
     * 列表相关事件实现方法。
     *
     * @param parentActivity
     * @param listView
     * @param listAdapter
     */
    protected void initListViewListenersImpl(Activity parentActivity, ListView listView, AbstractChattingListAdapter listAdapter){

        //** 长按弹出菜单事件注册和处理逻辑
        listView.setOnItemLongClickListener((parent, view, listItemPosition, id) -> {

            if (listAdapter.checkIndexValid(listItemPosition)) {
                // 长按事件对应的Listview中的数据单元
                final Message d = listAdapter.getListData().get(listItemPosition);
                if(d != null) {
                    // 准备好要弹出的菜单item
                    final ArrayList<MenuItem> menuItemList  = new ArrayList();
                    if(d.getMsgType() == MsgType.TYPE_TEXT) {
                        menuItemList.add(new MenuItem().setItem($$(R.string.abstract_chat_message_popmenu_copy)).setItemActionId(CONTEXT_MENU_ID_COPY).setItemResId(R.drawable.chatting_popmenu_copy_ico));
                    }
                    if(messageCanBeRevoke(d)) {
                        menuItemList.add(new MenuItem().setItem($$(R.string.abstract_chat_message_popmenu_revoke)).setItemActionId(CONTEXT_MENU_ID_REVOKE).setItemResId(R.drawable.chatting_popmenu_revoke_ico));
                    }
                    if(!d.isControl()) {
                        menuItemList.add(new MenuItem().setItem($$(R.string.abstract_chat_message_popmenu_delete)).setItemActionId(CONTEXT_MENU_ID_DELETE).setItemResId(R.drawable.chatting_popmenu_delete_ico));
                    }
                    if(d.isForwardEnabled()) {
                        menuItemList.add(new MenuItem().setItem($$(R.string.abstract_chat_message_popmenu_forward)).setItemActionId(CONTEXT_MENU_ID_FORWARD).setItemResId(R.drawable.chatting_popmenu_forward_ico));
                    }
                    if(d.isQuoteEnabled()) {
                        menuItemList.add(new MenuItem().setItem($$(R.string.abstract_chat_message_popmenu_quote)).setItemActionId(CONTEXT_MENU_ID_QUOTE).setItemResId(R.drawable.chatting_popmenu_quote_ico));
                    }

                    // 准备好弹出菜单对象
                    FloatMenu floatMenu = new FloatMenu(parentActivity, 118);
                    // 添加要显示的菜单项
                    floatMenu.items(menuItemList);
                    // 菜单项点击事件处理
                    floatMenu.setOnItemClickListener((v, menuItemPosition) -> {
                        MenuItem mi = menuItemList.get(menuItemPosition);
                        if(mi != null) {
                            switch (mi.getItemActionId()) {
                                // 复制内容
                                case CONTEXT_MENU_ID_COPY: {
                                    ToolKits.copyTextToClipborad(parentActivity, d.getText());
                                    WidgetUtils.showToast(parentActivity, $$(R.string.abstract_chat_message_copy_sucess), WidgetUtils.ToastType.OK);
                                    break;
                                }
                                // 撤回消息
                                case CONTEXT_MENU_ID_REVOKE: {
                                    doMessageRevoking(chatType, d, toId, toName);
                                    break;
                                }
                                // 删除消息
                                case CONTEXT_MENU_ID_DELETE:  {
                                    doMessageDelete(chatType, d.getFingerPrintOfProtocal(), toId);
                                    break;
                                }
                                // 转发消息
                                case CONTEXT_MENU_ID_FORWARD: {
                                    Intent i = IntentFactory.createTargetChooseActivityIntent(self()
                                            , TargetSource.latestChatting | TargetSource.friend | TargetSource.group
                                            , TargetSourceFilterFactory.createTargetSourceFilterLatestChatting4MsgForward(chatType, toId)
                                            , TargetSourceFilterFactory.createTargetSourceFilterFriend4MsgForward(chatType, toId)
                                            , TargetSourceFilterFactory.createTargetSourceFilterGroup4MsgForward(chatType, toId)
                                            , null
                                            , d
                                            , null);
                                    ActivityCompat.startActivityForResult(self(), i, ActivityRequestCode.REQUEST_CODE_FOR_FORWARD_TARGET_CHOOSE, null);
                                    break;
                                }
                                // 引用消息
                                case CONTEXT_MENU_ID_QUOTE: {
                                    doMessageQuote(d);
                                    break;
                                }
                            }
                        }
                    });

                    // 显示弹出菜单
                    floatMenu.show(floatMenuShowPoint);
                }
            }
            // return true if the callback consumed the long click, false otherwise
            return true; // 返回true就可解决与点击事件的冲突哦
        });
    }

    /**
     * 表情功能相关事件实现方法。
     *
     * @param faceBoardView
     * @param txtMsg
     */
    protected void initFaceListenersImpl(Activity parentActivity, FaceBoardView faceBoardView, EditText txtMsg){

        faceBoardView.setOnOperationListener(new FaceBoardView.FaceOnOperationListener() {
            @Override
            public void selectedIEmoji(FaceEntity iEmoji){
                System.out.println("-----表情事件---好友聊天点击了iEmoji="+iEmoji.getDesc());
                //图文混排专用类
                SpannableString ss = new SpannableString(iEmoji.getDesc());

                // 经测试getTextSize得到的已经是px，无需再转换。 *1.2 是因为得到的图片看起来比文字略小，所以增大20%
                int emojiSize = (int)(txtMsg.getTextSize() * 1.2);
                // 测试发现先获取drawable，再设置bounds的情况下，点击输入框后图片会恢复到drawable的原始大小，故应在创建drawable时即设置好尺寸
                Drawable iDrawable = EmojiUtil.getEmojiDrawableByDesc(parentActivity, iEmoji.getDesc(), emojiSize, emojiSize);
                ImageSpan is = new ImageSpan(iDrawable, DynamicDrawableSpan.ALIGN_BOTTOM);
                ss.setSpan(is,0,iEmoji.getDesc().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);//SPAN_EXCLUSIVE_EXCLUSIVE 不包括指定内容两边的东西
                //获取到光标的位置，确保插入位置追随光标
                int selectionEnd = txtMsg.getSelectionEnd();
                Editable finalText = txtMsg.getText();
                //追加输入的内容到原有内容上
                if (selectionEnd < finalText.length()){
                    finalText.insert(selectionEnd,ss);
                }else{
                    finalText.append(ss);
                }
                txtMsg.setText(finalText);
                //重新设置焦点位置
                txtMsg.setSelection(selectionEnd+ss.length());
            }

            @Override
            public void selectedBackSpaceInIEmoji(){
                System.out.println("-----表情事件---点击了模拟系统删除键");
                //模拟系统删除键
                txtMsg.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
                txtMsg.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,KeyEvent.KEYCODE_DEL));
            }
        });
    }

    /**
     * 显示消息撤回进度提示框。
     *
     * @param fpForMessage 被撤回消息对应的指纹码（如果是群聊，则此指纹码实际指的是父指纹码——即fingerPrintOfParent）
     */
    protected void showMessageRevokingProgess(String fpForMessage){
        if(this.messageRevokingDialogProgess != null){
            this.messageRevokingDialogProgess.hide(true, null);
            this.messageRevokingDialogProgess = null;
        }

        this.messageRevokingDialogProgess = new MessageRevokingDialogProgess(this){
            /**
             * 手机上按返回键后要做的事。
             * 默认调用 {@link #showProgressDialogForPairing(boolean)}，参数是false。
             */
            @Override
            protected void backPressed(){
                super.backPressed();
                // 进度提示框显示时，如果用户按返回按钮时，让app的表现相当于直接在当前activity上按返回键（就像微信那
                // 样），否则默认情况下进度提示框本身会响应back键，而无法做到直接退出activity（就无法做到跟微信一样了）
                if(!AbstractChattingActivity.this.isFinishing()) {
                    AbstractChattingActivity.this.finish();
                }
            }
        };
        this.messageRevokingDialogProgess.show(fpForMessage);
    }

    /**
     * 隐藏消息撤回进度提示框的显示。
     *
     * @param enforce true表示无条件强制进度提示框的显示，false表示只有当 fpForMessage 参数与当前正在撤回的指纹码一致才会取消显示哦
     * @param fpForMessage 被撤回消息对应的指纹码（如果是群聊，则此指纹码实际指的是父指纹码——即fingerPrintOfParent）
     */
    protected void hideMessageRevokingProgess(boolean enforce, String fpForMessage){
        if(this.messageRevokingDialogProgess != null){
            this.messageRevokingDialogProgess.hide(false, fpForMessage);
            this.messageRevokingDialogProgess = null;
        }
    }

    /**
     * 消息"撤回"功能实现。
     *
     * @param chatType 聊天类型，see {@link ChatType}
     * @param message  要撤回的消息位于聊天列表数据模型中的消息对象
     * @param toId 群聊时这表示群id，否则表示好友或陌生人uid
     * @param toName 本参数仅用于陌生人聊天模下表示发送者昵称，其它模式下请传null即可
     */
    protected void doMessageRevoking(int chatType, Message message, String toId, String toName){

        if(message == null) {
            Log.w(TAG, "【消息撤回】doMessageRevoking中，message == null！");
            WidgetUtils.showWithDialog(this, $$(R.string.general_error), $$(R.string.abstract_chat_message_canot_revoke));
            return;
        }

        //** 最终再次进行消息撤回时限检查，防止用户故意弹出菜单后，等超时时间过，仍然点击“撤回”菜单项的bug！
        boolean isGroupOwner = (chatType == ChatType.CHAT_TYPE_GROUP$CHAT && GroupsProvider.isThisGroupOwner(toId));
        // 群管理员可无条件、无时限地撤回所有消息（包括自已发出的、别人发出的），否则只能撤回规定时限内的消息
        if(!isGroupOwner){
            // 消息超限检查
            if(!messageIsNotTimeoutForRevoke(message)){
                WidgetUtils.showWithDialog(this, $$(R.string.general_prompt), MessageFormat.format($$(R.string.chat_message_has_timeout_for_revoke), Const.CHATTING_MESSAGE_CAN_BE_REVOKE_TIME));
                 return;
            }
        }

        boolean isGroupChat = (chatType == ChatType.CHAT_TYPE_GROUP$CHAT);
        // 被撤回消息对应的指纹码（也就是唯一消息ID啦）
        String fpForMessage = (chatType == ChatType.CHAT_TYPE_GROUP$CHAT ? message.getFingerPrintOfParent() :  message.getFingerPrintOfProtocal());
        // 显示撤回进度提示框
        showMessageRevokingProgess(fpForMessage);

        // 准备好将要发出的"撤回"指令的指令内容
        RevokedMeta contentForRevokeCMD = MessageRevokingManager.constructRevokedMetaForOperator(fpForMessage
                // 是群聊 且 撤回的是别人的消息时，需要传入被撤回消息发送者的uid
                , isGroupChat && !message.isOutgoing()?message.getSenderId():null
                // 是群聊 且 撤回的是别人的消息时，需要传入被撤回消息发送者的昵称
                , isGroupChat && !message.isOutgoing() ? message.getSenderDisplayName():null);
        if(contentForRevokeCMD == null)
            return;

        // 为将要发出的"撤回"指令准备好指纹码
        final String fpForRevokeCMD = Protocal.genFingerPrint();

        // 加入消息撤回管理器！
        MessageRevokingManager.MessageBeRevoke messageWillBeRevoke = MessageRevokingManager.MessageBeRevoke.create(chatType, toId,message);
        if(messageWillBeRevoke != null) {
            // 撤回管理器中执行"开始"撤回逻辑
            MyApplication.getInstance2().getIMClientManager().getMessageRevokingManager().revokeStart(fpForRevokeCMD, messageWillBeRevoke);

            // 发送撤回指令
            if(chatType == ChatType.CHAT_TYPE_FREIDN$CHAT)
                MessageHelper.sendRevokeMessageAsync(this, fpForRevokeCMD, toId, contentForRevokeCMD, null);
            else if(chatType == ChatType.CHAT_TYPE_GUEST$CHAT)
                TMessageHelper.sendRevokeMessageAsync(this, fpForRevokeCMD, toId, toName, contentForRevokeCMD, null);
            else if(chatType == ChatType.CHAT_TYPE_GROUP$CHAT)
                GMessageHelper.sendRevokeMessageAsync(this, fpForRevokeCMD, toId, contentForRevokeCMD, null);
            else
                Log.w(TAG, "【消息撤回】发送撤回指令，无效的chatType="+chatType);
        }
        else{
            Log.w(TAG, "【消息撤回】撤回指令发出前MessageBeRevoke.create后，messageBeRevoke==null!");
            WidgetUtils.showWithDialog(this, $$(R.string.general_error), "无法撤回，请重启应用后再试！");
        }
    }

    /**
     * 消息"删除"功能实现（有确认对话框）。
     *
     * @param chatType 聊天类型，see {@link ChatType}
     * @param fpForMessage  被删除消息的指纹码
     * @param forId 群聊时这表示群id，否则表示好友或陌生人uid
     */
    protected void doMessageDelete(int chatType, String fpForMessage, String forId){
        new AlertDialog.Builder(this)
                .setTitle(R.string.general_are_u_sure)
                .setMessage(R.string.chat_message_delete_prompt)
                .setPositiveButton(R.string.general_yes, (dialog, which) -> doMessageDeleteImpl(chatType, fpForMessage, forId))
                .setNegativeButton(R.string.general_no, null)
                .show();
    }

    /**
     * 消息"删除"功能实现。
     *
     * @param chatType 聊天类型，see {@link ChatType}
     * @param fpForMessage  被删除消息的指纹码
     * @param forId 群聊时这表示群id，否则表示好友或陌生人uid
     */
    protected void doMessageDeleteImpl(int chatType, String fpForMessage, String forId){
        MessagesProvider.RemoveResult result = null;

        // 先从内存中的消息列表中删除该消息对象（进而会通知ui层刷新聊天界面中的显示）
        int alrmType = -1;
        if(chatType == ChatType.CHAT_TYPE_FREIDN$CHAT || chatType == ChatType.CHAT_TYPE_GUEST$CHAT){
            result = MyApplication.getInstance2().getIMClientManager().getMessagesProvider().removeMessage(this, forId, fpForMessage, true);
        }
        else if(chatType == ChatType.CHAT_TYPE_GROUP$CHAT){
//            // 是否是世界频道（世界频道消息是不用存本地sqlite记录的）
//            boolean isWordGroup = GroupEntity.isWorldChat(forId);
//            result = MyApplication.getInstance2().getIMClientManager().getGroupsMessagesProvider().removeMessage(this, forId, fpForMessage, !isWordGroup);
            result = MyApplication.getInstance2().getIMClientManager().getGroupsMessagesProvider().removeMessage(this, forId, fpForMessage, true);
        }
        else{
            Log.w(TAG, "无效的chatType="+chatType+"，doMessageDelete无法继续！");
            return;
        }

        // 如果被删除的是最后一条消息，则要同时更新首页"消息"列表中的内容显示（应显示为被删除前的倒数第2条消息内容），
        // 否则该消息已被删除但首页列表中显示的还是已被删除消息内容，ui上看起来它就是个bug了，很不友好！
        if(result != null){
            // 上面的消息删除操作已成功，才需要继续余下的逻辑哦
            if(result.isDeletedSucess()){

                // 不同消息类型，对应的首页"消息"列表中的Alarm类型
                if(chatType == ChatType.CHAT_TYPE_FREIDN$CHAT){
                    alrmType = AlarmType.friendChat;
                }
                else if(chatType == ChatType.CHAT_TYPE_GUEST$CHAT){
                    alrmType = AlarmType.guestChat;
                }
                else if(chatType == ChatType.CHAT_TYPE_GROUP$CHAT){
                    alrmType = AlarmType.groupChat;
                }

                Log.i(TAG, "》》》》》》》》》》》》result.isLast()="+result.isLast());

                // 如果刚才删除的是该聊天会话的最后一条消息则就要更新首页消息列表的内容显示
                if(result.isLast()){
                    Message previousDeletedMessage = result.getPreviousDeletedMessage();
                    AlarmsProvider ap = MyApplication.getInstance2().getIMClientManager().getAlarmsProvider();

                    // 更新首页消息内容
                    String newAlarmContent = "";
                    if(previousDeletedMessage != null){
                        newAlarmContent = MessageExt.parseMessageContentPreview(
                                this, previousDeletedMessage.getText(), previousDeletedMessage.getMsgType());
                    }
                    ap.updateAlarmContentAndTime(alrmType, forId, newAlarmContent
                            , previousDeletedMessage==null?0:previousDeletedMessage.getDate(), true);
                }
                // 如果删除的消息上显示有时间，则要在该消息被删除后设置它的下一条消息显示时间哦
                else{
                    Message deletedMessage = result.getDeletedMessage();
                    Message behindDeletedMessage = result.getBehindDeletedMessage();
                    if(deletedMessage.isShowTopTime()) {
                        if (behindDeletedMessage != null)
                            behindDeletedMessage.setShowTopTime(true);
                    }
                }
            }
        }
    }

    /**
     * 消息"引用"功能实现。
     *
     * @param d  被引用的消息对象
     * @since 11.0
     */
    protected void doMessageQuote(Message d) {
        if(quote4InputWrapper != null)  {
            // 显示引用内容
            quote4InputWrapper.doQuote(chatType, toId, d);

            // 自动弹出输入法软键盘
            showInputMethodForEdit((o, arg) -> {
                // 然后再自动滚动到消息列表底部，体升体验
                AbstractChattingListAdapter cla =  getChattingListAdapter();
                if(cla != null) {
                    cla.showLastItem();
                }
            });
        }
    }

    /**
     * 取消设置指定的消息高亮显示一次（高亮特性目前仅用于搜索功能进到聊天界面时，设置搜索到到的包含关键字的消息）。
     * <p>
     * 本方法主要是为了解决：当高亮闪动动画未完成的情况下就退出聊天界面，从而导致原本由动画结束事件来重置该message对象
     * 的高亮字段值为false的逻辑不能实现，因为动画没完就从退出聊天界面了呀）。
     * 为了解决这个问题，将在聊天界面退出时，尝试强制重置高亮fp的message对象的高亮字段值为false，从而保证下次进到聊天
     * 界面时，不会因高亮字段值仍为true时而发生错误高亮的问题。
     *
     * @since 8.2
     */
    protected void undoHighlightOnceMessage(){
        try {
            // 需要被高亮度显示的消息指纹码（也就是消息唯一ID）
            String fpForMessage = highlightOnceMsgFingerprint;

            if (fpForMessage != null && toId != null) {
                Message m = null;
                // 如果是单聊
                if (chatType == ChatType.CHAT_TYPE_FREIDN$CHAT || chatType == ChatType.CHAT_TYPE_GUEST$CHAT) {
                    // 根据指纹码找到该消息所在聊天消息列表中的索引以及消息对象引用
                    m = imc().getMessagesProvider().findMessageByFingerPrint(toId, fpForMessage);
                }
                // 如果是群聊
                else if (chatType == ChatType.CHAT_TYPE_GROUP$CHAT) {
                    // 根据指纹码找到该消息所在聊天消息列表中的索引以及消息对象引用
                    m = imc().getGroupsMessagesProvider().findMessageByFingerPrint(toId, fpForMessage);
                } else {
                    Log.w(TAG, "无效的chatType=" + chatType + "，undoHighlightOnceMessage无法继续！");
                    return;
                }

                if (m != null) {
                    // 取消设置高亮
                    m.setHighlightOnce(false);
                }
            } else {
                Log.w(TAG, "undoHighlightOnceMessage()时无法继续，无效的参数：chatType=" + chatType + "，fpForMessage=" + fpForMessage + "，forId=" + toId);
            }
        } catch (Exception e){
            Log.w(TAG, e);
        }
    }

    /**
     * 设置指定的消息高亮显示一次、并让该条消息滚动到列表可视区（高亮特性目前仅用于搜索功能进到聊天界面（设置搜索到到的包含关键字的消息）和消息引用功能时）。
     *
     * @return true表示高亮成功，否则不成功
     * @since 8.2
     */
    protected boolean doHighlightOnceMessage(ListView listView) {
        // 需要被高亮度显示的消息指纹码（也就是消息唯一ID）
        String fpForMessage = highlightOnceMsgFingerprint;
        return doHighlightOnceMessage(listView, fpForMessage);
    }

    /**
     * 设置指定的消息高亮显示一次、并让该条消息滚动到列表可视区（高亮特性目前仅用于搜索功能进到聊天界面（设置搜索到到的包含关键字的消息）和消息引用功能时）。
     *
     * @return true表示高亮成功，否则不成功
     * @since 11.0
     */
    protected boolean doHighlightOnceMessage(ListView listView, String fpForMessage){
        if(listView == null || fpForMessage == null) {
            return false;
        }

        boolean sucess = false;
        try {
//            // 需要被高亮度显示的消息指纹码（也就是消息唯一ID）
//            String fpForMessage = highlightOnceMsgFingerprint;

            if (fpForMessage != null && toId != null) {
                MessagesProvider.FindResult r = null;
                // 如果是单聊
                if (chatType == ChatType.CHAT_TYPE_FREIDN$CHAT || chatType == ChatType.CHAT_TYPE_GUEST$CHAT) {
                    // 根据指纹码找到该消息所在聊天消息列表中的索引以及消息对象引用
                    r = imc().getMessagesProvider().findMessageByFingerPrintX(toId, fpForMessage);
                }
                // 如果是群聊
                else if (chatType == ChatType.CHAT_TYPE_GROUP$CHAT) {
                    // 根据指纹码找到该消息所在聊天消息列表中的索引以及消息对象引用
                    r = imc().getGroupsMessagesProvider().findMessageByFingerPrintX(toId, fpForMessage);
                } else {
                    Log.w(TAG, "无效的chatType=" + chatType + "，doHighlightOnceMessage无法继续！");
                    return false;
                }

                sucess = doHighlightOnceMessage(listView, r);

//                if (r != null && r.getMessage() != null) {
//                    // 让该消息滚动到列表的可视区
//                    if (listView != null)
//                        listView.setSelection(r.getIndex());
//                    // 设置高亮标识
//                    r.getMessage().setHighlightOnce(true);
//                    // 通过列表刷新
//                    notifyListDataSetChanged();
//                    // 设置高亮成功标识
//                    sucess = true;
//                }
            } else {
                Log.w(TAG, "doHighlightOnceMessage()时无法继续，无效的参数：chatType=" + chatType + "，fpForMessage=" + fpForMessage + "，forId=" + toId);
            }
        } catch (Exception e){
            Log.w(TAG, e);
        }

        return sucess;
    }

    /**
     * 设置指定的消息高亮显示一次、并让该条消息滚动到列表可视区（高亮特性目前仅用于搜索功能进到聊天界面（设置搜索到到的包含关键字的消息）和消息引用功能时）。
     *
     * @return true表示高亮成功，否则不成功
     * @since 11.0
     */
    protected boolean doHighlightOnceMessage(ListView listView, MessagesProvider.FindResult r){
        boolean sucess = false;
        if (r != null && r.getMessage() != null) {
            // 让该消息滚动到列表的可视区
            if (listView != null)
                listView.setSelection(r.getIndex());
            // 设置高亮标识
            r.getMessage().setHighlightOnce(true);
            // 通过列表刷新
            notifyListDataSetChanged();
            // 设置高亮成功标识
            sucess = true;
        }

        return sucess;
    }

    /**
     * 自动为文本消息输入框弹出输入法软键盘。
     *
     * @param o 弹出软键盘后要做的事可能通过此观察者实现，本参数可为null
     * @since 11.0
     */
    protected void showInputMethodForEdit(Observer o) {
        // 自动弹出输入法软键盘
        EditText msgEditText = getMsgEditText();
        if(msgEditText != null) {
            // 获取焦点，否则无法切换到键盘模式
            msgEditText.requestFocus();
            // 稍作延迟再弹出，否则没效果
            getWindow().getDecorView().postDelayed(() -> {
                // 显示输入法输软键盘
                ToolKits.showInputMethod(AbstractChattingActivity.this, msgEditText);

                if(o != null) {
                    o.update(null, null);
                }
//                // 然后再自动滚动到消息列表底部，体升体验
//                AbstractChattingListAdapter cla =  getChattingListAdapter();
//                if(cla != null) {
//                    cla.showLastItem();
//                }
            }, 200);// 因输入法的打开的关闭自动系统动画和延迟效果，此处加上延迟才会达到打开的效果
        }
    }

    protected void onActivityResult_shortVideoRecordComplete(Intent data){
        if(data != null) {
            String shortVideoPath = data.getStringExtra("path");
            long duration = data.getLongExtra("duration", 0);
            boolean reachedMaxRecordTime = data.getBooleanExtra("reachedMaxRecordTime", false);

            Log.i(TAG, "【短视频录制完成回调】录制完成(时长："+duration+"ms)，存放路径为："+shortVideoPath);

            new SendShortVideoProcessor(this, String.valueOf(this.chatType), this.toId, this.toName, shortVideoPath, duration, reachedMaxRecordTime).doSend();
        }
        else {
            Log.w(TAG, "无效的回调数据，发送短视频没有继续！");
        }
    }

    protected void onActivityResult_contactTargetSelectedComplete(
            Activity parentActivity, AbstractChattingListAdapter listAdapter, Intent data){

        if(data != null) {
            String toName = this.toName;
            String toId = this.toId;
            // 本字段仅用于单聊（好友聊天、陌生人聊天时），这个文件名目前仅用于Glide缓存图片时作为signature字段使用
            String toUserAvatarFileName = null;

            if(this.chatType == ChatType.CHAT_TYPE_FREIDN$CHAT) {
                RosterElementEntity toFriendInfo = imc().getFriendsListProvider().getFriendInfoByUid2(toId);
                // 为Null则有可能是这种情况：是从Notification点进来的，但该人员已经不是好友了（已删除）
                if (toFriendInfo == null)
                    return;

                toName = toFriendInfo.getNickNameWithRemark();
                toId = toFriendInfo.getUser_uid();
                toUserAvatarFileName = toFriendInfo == null?null:toFriendInfo.getUserAvatarFileName();
            } else if(this.chatType == ChatType.CHAT_TYPE_GUEST$CHAT){
                // 对于陌生人来说，extra1String中，存放的就是可能最新头像文件名（在查看最新用户资料时设置进来的）
                toUserAvatarFileName = imc().getAlarmsProvider().getExtra1String(AlarmType.guestChat, toId);
            }
            else if(this.chatType == ChatType.CHAT_TYPE_GROUP$CHAT) {
//              toId = (isWorldChat()?"世界频道无ID": toId);
            }

            //**---- 从选择界面中返回的选中数据
            TargetEntity ue = (TargetEntity)data.getSerializableExtra("selected_user");
            if(ue != null) {
                int cardType = ContactMeta.TYPE_USER;
                if(ue.getTargetChatType() == ChatType.CHAT_TYPE_GROUP$CHAT) {
                    cardType = ContactMeta.TYPE_GROUP;
                }

                final ContactMeta selectedCard = new ContactMeta(cardType, ue.getTargetId(), ue.getTargetName(), ue.getTargetOtherInfo());
//              final ContactMeta selectedContactUser = (ContactMeta)data.getSerializableExtra("selected_user");

                //**---- 显示发送确认界面
                showContactTargetChooseResultConfirm(parentActivity, listAdapter, this.chatType, toName, toId, toUserAvatarFileName, selectedCard);
            } else {
                WidgetUtils.showWithDialog(self(), $$(R.string.general_prompt), $$(R.string.general_invalid_data));
            }
        }
    }
    private static void showContactTargetChooseResultConfirm(Activity parentActivity, AbstractChattingListAdapter listAdapter
            , int chatType, String toName, String toId, String toUserAvatarFileName, final ContactMeta selectedCard){

        final String finalToId = toId;
        final String finalToName = toName;
        // 本字段仅用于单聊（好友聊天、陌生人聊天时），这个文件名目前仅用于Glide缓存图片时作为signature字段使用
        final String userAvatarFileName = toUserAvatarFileName;

        // 名片类型
        String targetResultTypeName = "[" +parentActivity.getResources().getString(selectedCard.getType() == ContactMeta.TYPE_USER?R.string.general_card_user_desc:R.string.general_card_group_desc)+ "]";
        // 描述信息
        String targetResultDesc = targetResultTypeName +" "+selectedCard.getNickName();

        new TargetChooseResultConfirm(parentActivity, parentActivity.getResources().getString(R.string.abstract_chat_message_send_to), toName, toId, targetResultDesc) {

            // 显示"发送到"头像的实现方法
            @Override
            protected void loadHeadIcon(ImageView toImageView) {
                if(chatType == ChatType.CHAT_TYPE_GROUP$CHAT){
                    // 加载群头像（跳过内存缓存的目的，是希望借此机会强制加载最新群头像1次哦）
                    ImageCacheLoader.loadGroupImgWithGlide(Glide.with(parentActivity), finalToId, toImageView, 0 , true, -1);
                }
                else {
                    boolean dontDiskCache = false;
                    // 该字段为空可能是该用户没有设置头像，也可能是有可能是对方已把"我"删除，因而对方不在我
                    // 的好友列表里了，所以好友数据不存在，头像文件名也就取不到了，这种情况取头像时，就把它当陌生人聊
                    // 天模式去取头像，即dontDiskCache为true——确保相同url情况下至少app重启后能强行拉取一次最新头像
                    if (CommonUtils.isStringEmpty(userAvatarFileName, true)) {
                        dontDiskCache = true;
                    }
                    // 加载用户头像(有头像文件名fileNameForUserAvatar作为Glide加载时的signature，可以保证在好友更改头像后，能及时加载到最新图片)
                    ImageCacheLoader.loadAvatarImgWithGlide(parentActivity, toId, userAvatarFileName, toImageView, 25, R.drawable.default_avatar_yuan_50_3x, dontDiskCache, false);
                }
            }

            // 点击"确认"按钮的实现方法
            @Override
            protected void doClickOK(final String toSayContent) {
                //** 尝试发出附带的"留言"消息的观察者对象
                Observer obsAfterSucess = createToSayObserver(parentActivity, chatType, finalToId, finalToName, toSayContent);

                //** 真正发出"名片"消息
                if(chatType == ChatType.CHAT_TYPE_FREIDN$CHAT)
                    MessageHelper.sendContactMessageAsync(parentActivity, finalToId, selectedCard, obsAfterSucess);
                else if(chatType == ChatType.CHAT_TYPE_GUEST$CHAT)
                    TMessageHelper.sendContactMessageAsync(parentActivity, finalToId, finalToName, selectedCard, obsAfterSucess);
                else if(chatType == ChatType.CHAT_TYPE_GROUP$CHAT)
                    GMessageHelper.sendContactMessageAsync(parentActivity, finalToId, selectedCard, obsAfterSucess);
            }

            // "确认"按钮逻辑执行完成后的额外处理
            @Override
            protected void afterClickOK(EditText toSay) {
                //** 最后做出相应的UI复位工作
                // 如果输入法已开启，无条件关闭之
                ToolKits.hideInputMethod(parentActivity, toSay);
                // 消息列表首先自动滚动到最后一行
                listAdapter.showLastItem();
            }

            // 点击"取消"按钮的实现方法
            @Override
            protected void doClickCancel(EditText toSay) {
                // 如果输入法已开启，无条件关闭之
                ToolKits.hideInputMethod(parentActivity, toSay);
            }
        };
    }

    protected void onActivityResult_forwardTargetSelectedComplete(
            Activity parentActivity, AbstractChattingListAdapter listAdapter, Intent data){

        if(data != null) {

            //**---- 从选择界面中返回的选中数据
            TargetEntity ue = (TargetEntity)data.getSerializableExtra("selected_user");
            Message beForwardMsg = (Message)data.getSerializableExtra("extra_obj");
            if(ue == null || beForwardMsg == null) {
                Log.w(TAG, "无效的数据！（ue="+ue+"、beForwardMsg="+beForwardMsg+"）");
                WidgetUtils.showWithDialog(self(), $$(R.string.general_prompt), $$(R.string.general_invalid_data));
                return;
            }

            String targetName = ue.getTargetName();
            String targetId = ue.getTargetId();
            // 本字段仅用于单聊（好友聊天、陌生人聊天时），这个文件名目前仅用于Glide缓存图片时作为signature字段使用
            String targetUserAvatarFileName = null;

            if(ue.getTargetChatType() == ChatType.CHAT_TYPE_FREIDN$CHAT) {
                RosterElementEntity toFriendInfo = imc().getFriendsListProvider().getFriendInfoByUid2(targetId);
                if (toFriendInfo == null)
                    return;

                targetName = toFriendInfo.getNickNameWithRemark();
                targetUserAvatarFileName = toFriendInfo == null?null:toFriendInfo.getUserAvatarFileName();
            } else if(ue.getTargetChatType() == ChatType.CHAT_TYPE_GUEST$CHAT) {
                // 对于陌生人来说，extra1String中，存放的就是可能最新头像文件名（在查看最新用户资料时设置进来的）
                targetUserAvatarFileName = imc().getAlarmsProvider().getExtra1String(AlarmType.guestChat, targetId);
            }

            //**---- 显示发送确认界面
            showForwardTargetChooseResultConfirm(parentActivity, listAdapter, ue.getTargetChatType(), targetName, targetId, targetUserAvatarFileName, beForwardMsg);
        }
    }
    private static void showForwardTargetChooseResultConfirm(Activity parentActivity, AbstractChattingListAdapter listAdapter
            , final int targetType, String toName, String toId, String toUserAvatarFileName, Message beForwardMsg) {

        final String finalToId = toId;
        final String finalToName = toName;
        // 本字段仅用于单聊（好友聊天、陌生人聊天时），这个文件名目前仅用于Glide缓存图片时作为signature字段使用
        final String userAvatarFileName = toUserAvatarFileName;

        // 消息内容预览
        String targetResultDesc = MessageExt.parseMessageContentPreview(parentActivity, beForwardMsg.getText(), beForwardMsg.getMsgType());

        new TargetChooseResultConfirm(parentActivity, parentActivity.getResources().getString(R.string.abstract_chat_message_forward_to), toName, toId, targetResultDesc) {

            // 显示"发送到"头像的实现方法
            @Override
            protected void loadHeadIcon(ImageView toImageView) {
                if(targetType == ChatType.CHAT_TYPE_GROUP$CHAT){
                    // 加载群头像（跳过内存缓存的目的，是希望借此机会强制加载最新群头像1次哦）
                    ImageCacheLoader.loadGroupImgWithGlide(Glide.with(parentActivity), finalToId, toImageView, 0 , true, -1);
                } else {
                    boolean dontDiskCache = false;
                    // 该字段为空可能是该用户没有设置头像，也可能是有可能是对方已把"我"删除，因而对方不在我
                    // 的好友列表里了，所以好友数据不存在，头像文件名也就取不到了，这种情况取头像时，就把它当陌生人聊
                    // 天模式去取头像，即dontDiskCache为true——确保相同url情况下至少app重启后能强行拉取一次最新头像
                    if (CommonUtils.isStringEmpty(userAvatarFileName, true)) {
                        dontDiskCache = true;
                    }
                    // 加载用户头像(有头像文件名fileNameForUserAvatar作为Glide加载时的signature，可以保证在好友更改头像后，能及时加载到最新图片)
                    ImageCacheLoader.loadAvatarImgWithGlide(parentActivity, toId, userAvatarFileName, toImageView, 25, R.drawable.default_avatar_yuan_50_3x, dontDiskCache, false);
                }
            }

            @Override
            protected Observer createToSayObserver(final Activity parentActivity, final int chatType, final String finalToId, final String finalToName, final String toSayContent){
                return (observable, data1) -> {
                    if(data1 != null && data1 instanceof Message) {
                        // 转发的消息不需要处理文件上传，直接默认就认为文件已经上传好了（因为功能就只允许转发成功发出的文件消息）
                        ((Message)data1).setSendStatusSecondary(Message.SendStatusSecondary.processOk);
                        // 设置标识，以便消息发送失败时，用户点击重传时进行特殊的逻辑处理
                        ((Message)data1).setForwardOutgoing(true);
                    }

                    //** 尝试发出附带的"留言"消息
                    super.toSay(parentActivity, chatType, finalToId, finalToName, toSayContent);
                };
            }

            // 点击"确认"按钮的实现方法
            @Override
            protected void doClickOK(final String toSayContent) {
                //** 尝试发出附带的"留言"消息
                Observer obsAfterSucess = createToSayObserver(parentActivity, targetType, finalToId, finalToName, toSayContent);

                //** 真正发出"名片"消息
                if(targetType == ChatType.CHAT_TYPE_FREIDN$CHAT)
                    MessageForwardHelper.forward(parentActivity, beForwardMsg, finalToId, obsAfterSucess);
                else if(targetType == ChatType.CHAT_TYPE_GUEST$CHAT)
                    TMessageForwardHelper.forward(parentActivity, beForwardMsg, finalToId, finalToName, obsAfterSucess);
                else if(targetType == ChatType.CHAT_TYPE_GROUP$CHAT)
                    GMessageForwardHelper.forward(parentActivity, beForwardMsg, finalToId, obsAfterSucess);
            }

            // "确认"按钮逻辑执行完成后的额外处理
            @Override
            protected void afterClickOK(EditText toSay) {
                com.x52im.rbchat.utils.ToolKits.showCommonTosat(parentActivity, true, parentActivity.getResources().getString(R.string.abstract_chat_message_forward_complete), Toast.LENGTH_SHORT);
//              WidgetUtils.showToast(parentActivity, "转发完成", WidgetUtils.ToastType.OK);
            }

            // 点击"取消"按钮的实现方法
            @Override
            protected void doClickCancel(EditText toSay) {
            }
        };
    }

    protected void onActivityResult_locationSelectedComplete(Activity parentActivity, Intent data){
        if(data != null)
        {
            if(chatType == ChatType.CHAT_TYPE_FREIDN$CHAT) {
                // 不是好友，有可能是这种情况：是从Notification点进来的，但该人员已经不是好友了（已删除）
                if (!imc().getFriendsListProvider().isUserInRoster2(this.toId))
                    return;
            }

            //**---- 从选择界面中返回的选中数据
            final LocationMeta lm = (LocationMeta)data.getSerializableExtra("selected_location");
            if(lm != null) {
                Log.d(TAG, "【好友聊天】马上发出位置消息指令：-> " + lm.getLocationTitle() + " ," + lm.getLocationContent() + " ,"
                        + "经度：" + lm.getLongitude() + "，" + "纬度：" + lm.getLatitude()
                        +", 图片："+lm.getPrewviewImgFileName());

                //** 发出"位置"消息
                if(chatType == ChatType.CHAT_TYPE_FREIDN$CHAT)
                    MessageHelper.sendLocationMessageAsync(parentActivity, this.toId, lm, null);
                else if(chatType == ChatType.CHAT_TYPE_GUEST$CHAT)
                    TMessageHelper.sendLocationMessageAsync(parentActivity, this.toId, this.toName, lm, null);
                else if(chatType == ChatType.CHAT_TYPE_GROUP$CHAT)
                    GMessageHelper.sendLocationMessageAsync(parentActivity, this.toId, lm, null);
            }
            else
            {
                WidgetUtils.showWithDialog(this, $$(R.string.general_prompt), $$(R.string.get_location_is_null));
            }
        }
    }

    // 长按消息气泡事件处理(重写父类的本方法，目的是保存仿微信的长按弹出浮动菜单的显示基准位置点，以便设置弹出菜单的起始坐标)
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev)
    {
        if(ev.getAction() == MotionEvent.ACTION_DOWN)
        {
            // 保存此时的长按点信息
            floatMenuShowPoint.x = (int) ev.getRawX();
            floatMenuShowPoint.y = (int) ev.getRawY();
        }

        try {
            return super.dispatchTouchEvent(ev);
        }
        catch (Exception e)
        {
            Log.w(TAG, e);
            return false;
        }
    }

    /**
     * 获得消息输入框引用（子类需实现本抽象方法）。
     *
     * @return
     */
    protected abstract EditText getMsgEditText();

    /**
     * 获得聊天界面列表Adapter对象的引用（子类需实现本抽象方法）。
     *
     * @return
     */
    protected abstract AbstractChattingListAdapter getChattingListAdapter();

    /**
     * 刷新聊天列表（子类需实现本抽象方法）。
     */
    protected abstract void  notifyListDataSetChanged();

    @Override
    protected DataFromServer queryData(String... arg0) {
        return null;
    }

    @Override
    protected void refreshToView(Object dataToView) {
    }

    /**
     * 该消息是否可被撤回（子类可重写本方法实现自已的“撤回”功能权限可用逻辑）.
     *
     * @param d 聊天消息数据对象
     * @return true表示该消息可被撤回
     */
    protected boolean messageCanBeRevoke(Message d) {
        if (d != null) {
            // 只能撤回自已发出的 且 在撤回时限内的消息
            if(d.isRevokeEnabled() && d.getFingerPrintOfProtocal() != null){
                return messageIsNotTimeoutForRevoke(d);
            }
        }

        return false;
    }

    /** 是否是世界频道 */
    protected boolean isWorldChat() {
        return GroupEntity.isWorldChat(this.toId);
    }

    //----------------------------------------------------------------- 其它实用静态工具方法

    /**
     * 该消息是否未超出撤回时限.
     *
     * @param d 聊天消息数据对象
     * @return true表示该消息在超时时限内
     */
    public static boolean messageIsNotTimeoutForRevoke(Message d) {
        long cur = System.currentTimeMillis();
        if (d.getDate() > 0 && cur - d.getDate() < Const.CHATTING_MESSAGE_CAN_BE_REVOKE_TIME * 60 * 1000) {
            return true;
        }
        return false;
    }
}
